import os
import sys
from copy import deepcopy
from random import random

from shapely import geometry as shgeo

import cubicsuperpath
import inkex
import simpletransform

from .i18n import N_, _
from .svg import (apply_transforms, generate_unique_id,
                  get_correction_transform, get_document, get_node_transform)
from .svg.tags import (CONNECTION_END, CONNECTION_START, CONNECTOR_TYPE,
                       INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_DEFS_TAG,
                       SVG_GROUP_TAG, SVG_PATH_TAG, SVG_SYMBOL_TAG,
                       SVG_USE_TAG, XLINK_HREF)
from .utils import Point, cache, get_bundled_dir

COMMANDS = {
    # L10N command attached to an object
    "fill_start": N_("Fill stitch starting position"),

    # L10N command attached to an object
    "fill_end": N_("Fill stitch ending position"),

    # L10N command attached to an object
    "satin_start": N_("Auto-route satin stitch starting position"),

    # L10N command attached to an object
    "satin_end": N_("Auto-route satin stitch ending position"),

    # L10N command attached to an object
    "stop": N_("Stop (pause machine) after sewing this object"),

    # L10N command attached to an object
    "trim": N_("Trim thread after sewing this object"),

    # L10N command attached to an object
    "ignore_object": N_("Ignore this object (do not stitch)"),

    # L10N command attached to an object
    "satin_cut_point": N_("Satin cut point (use with Cut Satin Column)"),


    # L10N command that affects a layer
    "ignore_layer": N_("Ignore layer (do not stitch any objects in this layer)"),

    # L10N command that affects entire document
    "origin": N_("Origin for exported embroidery files"),

    # L10N command that affects entire document
    "stop_position": N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."),
}

OBJECT_COMMANDS = ["fill_start", "fill_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"]
LAYER_COMMANDS = ["ignore_layer"]
GLOBAL_COMMANDS = ["origin", "stop_position"]


class CommandParseError(Exception):
    pass


class BaseCommand(object):
    @property
    @cache
    def description(self):
        return get_command_description(self.command)

    def parse_symbol(self):
        if self.symbol.tag != SVG_SYMBOL_TAG:
            raise CommandParseError("use points to non-symbol")

        self.command = self.symbol.get('id')

        if self.command.startswith('inkstitch_'):
            self.command = self.command[10:]
        else:
            raise CommandParseError("symbol is not an Ink/Stitch command")

    def get_node_by_url(self, url):
        # url will be #path12345.  Find the corresponding object.
        if url is None:
            raise CommandParseError("url is None")

        if not url.startswith('#'):
            raise CommandParseError("invalid connection url: %s" % url)

        id = url[1:]

        try:
            return self.svg.xpath(".//*[@id='%s']" % id)[0]
        except (IndexError, AttributeError):
            raise CommandParseError("could not find node by url %s" % id)


class Command(BaseCommand):
    def __init__(self, connector):
        self.connector = connector
        self.svg = self.connector.getroottree().getroot()

        self.parse_command()

    def parse_connector_path(self):
        path = cubicsuperpath.parsePath(self.connector.get('d'))
        return apply_transforms(path, self.connector)

    def parse_command(self):
        path = self.parse_connector_path()

        neighbors = [
            (self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]),
            (self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1])
        ]

        if neighbors[0][0].tag != SVG_USE_TAG:
            neighbors.reverse()

        if neighbors[0][0].tag != SVG_USE_TAG:
            raise CommandParseError("connector does not point to a use tag")

        self.use = neighbors[0][0]

        self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF))
        self.parse_symbol()

        self.target = neighbors[1][0]
        self.target_point = neighbors[1][1]

    def __repr__(self):
        return "Command('%s', %s)" % (self.command, self.target_point)


class StandaloneCommand(BaseCommand):
    def __init__(self, use):
        self.node = use
        self.svg = self.node.getroottree().getroot()

        self.parse_command()

    def parse_command(self):
        self.symbol = self.get_node_by_url(self.node.get(XLINK_HREF))

        if self.symbol.tag != SVG_SYMBOL_TAG:
            raise CommandParseError("use points to non-symbol")

        self.parse_symbol()

    @property
    @cache
    def point(self):
        pos = [float(self.node.get("x", 0)), float(self.node.get("y", 0))]
        transform = get_node_transform(self.node)
        simpletransform.applyTransformToPoint(transform, pos)

        return Point(*pos)


def get_command_description(command):
    return COMMANDS[command]


def find_commands(node):
    """Find the symbols this node is connected to and return them as Commands"""

    # find all paths that have this object as a connection
    xpath = ".//*[@inkscape:connection-start='#%(id)s' or @inkscape:connection-end='#%(id)s']" % dict(id=node.get('id'))
    connectors = node.getroottree().getroot().xpath(xpath, namespaces=inkex.NSS)

    # try to turn them into commands
    commands = []
    for connector in connectors:
        try:
            commands.append(Command(connector))
        except CommandParseError:
            # Parsing the connector failed, meaning it's not actually an Ink/Stitch command.
            pass

    return commands


def layer_commands(layer, command):
    """Find standalone (unconnected) command symbols in this layer."""

    for global_command in global_commands(layer.getroottree().getroot(), command):
        if layer in global_command.node.iterancestors():
            yield global_command


def global_commands(svg, command):
    """Find standalone (unconnected) command symbols anywhere in the document."""

    for standalone_command in _standalone_commands(svg):
        if standalone_command.command == command:
            yield standalone_command


@cache
def global_command(svg, command):
    """Find a single command of the specified type.

    If more than one is found, print an error and exit.
    """

    commands = list(global_commands(svg, command))

    if len(commands) == 1:
        return commands[0]
    elif len(commands) > 1:
        print >> sys.stderr, _("Error: there is more than one %(command)s command in the document, but there can only be one.  "
                               "Please remove all but one.") % dict(command=command)

        # L10N This is a continuation of the previous error message, letting the user know
        # what command we're talking about since we don't normally expose the actual
        # command name to them.  Contents of %(description)s are in a separate translation
        # string.
        print >> sys.stderr, _("%(command)s: %(description)s") % dict(command=command, description=_(get_command_description(command)))

        sys.exit(1)
    else:
        return None


def _standalone_commands(svg):
    """Find all unconnected command symbols in the SVG."""

    xpath = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]"
    symbols = svg.xpath(xpath, namespaces=inkex.NSS)

    for symbol in symbols:
        try:
            yield StandaloneCommand(symbol)
        except CommandParseError:
            pass


def is_command(node):
    return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib


@cache
def symbols_path():
    return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg")


@cache
def symbols_svg():
    with open(symbols_path()) as symbols_file:
        return inkex.etree.parse(symbols_file)


@cache
def symbol_defs():
    return get_defs(symbols_svg())


@cache
def get_defs(document):
    defs = document.find(SVG_DEFS_TAG)

    if defs is None:
        defs = inkex.etree.SubElement(document, SVG_DEFS_TAG)

    return defs


def ensure_symbol(document, command):
    """Make sure the command's symbol definition exists in the <svg:defs> tag."""

    path = "./*[@id='inkstitch_%s']" % command
    defs = get_defs(document)
    if defs.find(path) is None:
        defs.append(deepcopy(symbol_defs().find(path)))


def add_group(document, node, command):
    return inkex.etree.SubElement(
        node.getparent(),
        SVG_GROUP_TAG,
        {
            "id": generate_unique_id(document, "group"),
            INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
            "transform": get_correction_transform(node)
        })


def add_connector(document, symbol, element):
    # I'd like it if I could position the connector endpoint nicely but inkscape just
    # moves it to the element's center immediately after the extension runs.
    start_pos = (symbol.get('x'), symbol.get('y'))
    end_pos = element.shape.centroid

    # Make sure the element's XML node has an id so that we can reference it.
    if element.node.get('id') is None:
        element.node.set('id', generate_unique_id(document, "object"))

    path = inkex.etree.Element(SVG_PATH_TAG,
                               {
                                   "id": generate_unique_id(document, "connector"),
                                   "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
                                   "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
                                   CONNECTION_START: "#%s" % symbol.get('id'),
                                   CONNECTION_END: "#%s" % element.node.get('id'),
                                   CONNECTOR_TYPE: "polyline",

                                   # l10n: the name of the line that connects a command to the object it applies to
                                   INKSCAPE_LABEL: _("connector")
                               })

    symbol.getparent().insert(0, path)


def add_symbol(document, group, command, pos):
    return inkex.etree.SubElement(group, SVG_USE_TAG,
                                  {
                                      "id": generate_unique_id(document, "use"),
                                      XLINK_HREF: "#inkstitch_%s" % command,
                                      "height": "100%",
                                      "width": "100%",
                                      "x": str(pos.x),
                                      "y": str(pos.y),

                                      # l10n: the name of a command symbol (example: scissors icon for trim command)
                                      INKSCAPE_LABEL: _("command marker"),
                                  })


def get_command_pos(element, index, total):
    # Put command symbols 30 pixels out from the shape, spaced evenly around it.

    # get a line running 30 pixels out from the shape
    outline = element.shape.buffer(30).exterior

    # find the top center point on the outline and start there
    top_center = shgeo.Point(outline.centroid.x, outline.bounds[1])
    start_position = outline.project(top_center, normalized=True)

    # pick this item's spot around the outline and perturb it a bit to avoid
    # stacking up commands if they add commands multiple times
    position = index / float(total)
    position += random() * 0.05
    position += start_position

    return outline.interpolate(position, normalized=True)


def remove_legacy_param(element, command):
    if command == "trim" or command == "stop":
        # If they had the old "TRIM after" or "STOP after" attributes set,
        # automatically delete them.  The new commands will do the same
        # thing.
        #
        # If we didn't delete these here, then things would get confusing.
        # If the user were to delete a "trim" symbol added by this extension
        # but the "embroider_trim_after" attribute is still set, then the
        # trim would keep happening.

        attribute = "embroider_%s_after" % command

        if attribute in element.node.attrib:
            del element.node.attrib[attribute]

        # Attributes have changed to be namespaced.
        # Let's check for them as well, they might have automatically changed.
        attribute = INKSTITCH_ATTRIBS["%s_after" % command]

        if attribute in element.node.attrib:
            del element.node.attrib[attribute]


def add_commands(element, commands):
    document = get_document(element.node)

    for i, command in enumerate(commands):
        ensure_symbol(document, command)
        remove_legacy_param(element, command)

        group = add_group(document, element.node, command)
        pos = get_command_pos(element, i, len(commands))
        symbol = add_symbol(document, group, command, pos)
        add_connector(document, symbol, element)


def add_layer_commands(layer, commands):
    document = get_document(layer)
    correction_transform = get_correction_transform(layer)

    for command in commands:
        ensure_symbol(document, command)
        inkex.etree.SubElement(layer, SVG_USE_TAG,
                               {
                                   "id": generate_unique_id(document, "use"),
                                   INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
                                   XLINK_HREF: "#inkstitch_%s" % command,
                                   "height": "100%",
                                   "width": "100%",
                                   "x": "0",
                                   "y": "-10",
                                   "transform": correction_transform
                               })
